import abc
import weakref
from collections.abc import Callable, Iterable
from typing import Any, TypeVar

from zkl_pyutils_registries import ListRegistry, SubTypehintMatcher, TolerantMatcher

from .scanning import add_onthefly, ensure_after, scan_agg, scan_call

AnyCarry = TypeVar('AnyCarry')
AnyPlugin = TypeVar('AnyPlugin')


class DockPlugin(abc.ABC):
    _dock_ref: weakref.ref['Dock']

    @property
    def dock(self) -> 'Dock':
        try:
            dock_ref = self._dock_ref
        except AttributeError:
            raise RuntimeError(f"DockPlugin.dock is accessed before installation!")

        dock = dock_ref()
        if dock is None:
            raise RuntimeError(f"DockPlugin.dock is accessed when the dock has been garbage collected!")

        return dock

    def on_installed(self, dock: 'Dock'):
        try:
            dock_ref = self._dock_ref
        except AttributeError:
            dock_ref = None

        if dock_ref is not None:
            raise RuntimeError(f"DockPlugin {self} cannot be installed into multiple docks!")

        self._dock_ref = weakref.ref(dock)


class Dock:
    def __init__(self):
        matcher = SubTypehintMatcher()
        matcher = TolerantMatcher(matcher)
        registry = ListRegistry(matcher)
        self._registry = registry

    def install(self,
        plugin: AnyPlugin, *,
        onthefly: bool = True,
    ):
        if plugin in self._registry.match(type(plugin)):
            return  # already installed, skip

        self._registry.register(type(plugin), plugin)

        if onthefly:
            add_onthefly(self, (plugin,))

        if isinstance(plugin, DockPlugin):
            plugin.on_installed(self)

    def get_plugin(self,
        query: type[AnyPlugin] | Any, *,
        singleton: bool = True,
        required: bool = True,
        after: bool = False,
    ) -> AnyPlugin | None:
        singleton_plugin = None
        for plugin in self.iter_plugins(query):
            if not singleton:
                if after:
                    self.ensure_after_plugins((plugin,))
                return plugin
            if singleton_plugin is not None:
                raise ValueError(f"Multiple plugins {query} are installed!")
            singleton_plugin = plugin
        if singleton_plugin is None:
            if required:
                raise ValueError(f"Required plugin {query} is not installed!")
        if singleton_plugin is not None and after:
            self.ensure_after_plugins((singleton_plugin,))
        return singleton_plugin

    def iter_plugins(self,
        query: type[AnyPlugin] | Any = Any, *,
        after: bool = False,
    ) -> Iterable[AnyPlugin]:
        for plugin in self._registry.match(query):
            if after:
                self.ensure_after_plugins((plugin,))
            yield plugin

    def ensure_after_plugins(self,
        plugins: Iterable[AnyPlugin]
    ):
        ensure_after(self, plugins)

    def scan_call_plugins(self,
        plugins: Iterable[AnyPlugin],
        func: Callable[[AnyPlugin], Any],
    ):
        scan_call(self, plugins, func)

    def scan_agg_plugins(self,
        plugins: Iterable[AnyPlugin],
        carry: AnyCarry,
        func: Callable[[AnyPlugin, AnyCarry], AnyCarry],
    ) -> AnyCarry:
        return scan_agg(self, plugins, carry, func)
